import { NextResponse } from 'next/server' import crypto from 'node:crypto' import type { EntityManager } from '@mikro-orm/postgresql' import type { AwilixContainer } from 'awilix' import { z } from 'zod' import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find' import { readJsonSafe } from '@open-mercato/shared/lib/http/readJsonSafe' import { createRequestContainer } from '@open-mercato/shared/lib/di/container' import { GatewayPaymentLink, GatewayPaymentLinkTransaction } from '@open-mercato/core/modules/payment_gateways/data/entities' import { readPaymentLinkStoredMetadata } from '@open-mercato/core/modules/payment_gateways/lib/payment-link-page-metadata' import { buildPaymentLinkUrl } from '@open-mercato/core/modules/payment_gateways/lib/payment-links' import type { PaymentGatewayService } from '@open-mercato/core/modules/payment_gateways/lib/gateway-service' import { emitPaymentGatewayEvent } from '@open-mercato/core/modules/payment_gateways/events' const sessionPayloadSchema = z.object({ email: z.string().email(), firstName: z.string().max(200).optional(), lastName: z.string().max(200).optional(), phone: z.string().max(50).optional(), companyName: z.string().max(200).optional(), acceptedTerms: z.boolean().optional(), customFormFields: z.record(z.string(), z.unknown()).optional(), }) type RequestContainer = AwilixContainer & { resolve(name: 'em'): EntityManager resolve(name: 'paymentGatewayService'): PaymentGatewayService } function buildPaymentLinkReturnUrl(baseUrl: string, state: 'success' | 'cancelled'): string { const url = new URL(baseUrl) url.searchParams.set('checkout', state) return url.toString() } export const metadata = { path: '/payment_link_pages/pay/[token]/session', POST: { requireAuth: false }, } export async function POST(req: Request, { params }: { params: Promise<{ token: string }> | { token: string } }) { try { const { token } = await params if (!token) { return NextResponse.json({ error: 'Payment link token is required' }, { status: 400 }) } const payload = await readJsonSafe(req) const parsed = sessionPayloadSchema.safeParse(payload) if (!parsed.success) { const flattened = parsed.error.flatten() return NextResponse.json({ error: 'Invalid payload', details: flattened, }, { status: 422 }) } const container = await createRequestContainer() const em = (container as RequestContainer).resolve('em') const link = await findOneWithDecryption(em, GatewayPaymentLink, { token, deletedAt: null }) if (!link) { return NextResponse.json({ error: 'Payment link not found' }, { status: 404 }) } if (link.linkMode !== 'multi') { return NextResponse.json({ error: 'This payment link does not support session creation' }, { status: 409 }) } if (link.status !== 'active') { return NextResponse.json({ error: 'This payment link is no longer active' }, { status: 409 }) } if (typeof link.maxUses === 'number' && link.maxUses > 0 && link.useCount >= link.maxUses) { return NextResponse.json({ error: 'This payment link has reached its maximum number of uses' }, { status: 409 }) } const storedMetadata = readPaymentLinkStoredMetadata(link.metadata) const sessionParams = storedMetadata.sessionParams if (!sessionParams || !sessionParams.providerKey) { return NextResponse.json({ error: 'Payment link session configuration is missing' }, { status: 500 }) } const scope = { organizationId: link.organizationId, tenantId: link.tenantId } const service = (container as RequestContainer).resolve('paymentGatewayService') const paymentLinkUrl = buildPaymentLinkUrl(new URL(req.url).origin, link.token) const { transaction, session } = await service.createPaymentSession({ providerKey: sessionParams.providerKey, paymentId: crypto.randomUUID(), amount: sessionParams.amount, currencyCode: sessionParams.currencyCode, captureMethod: sessionParams.captureMethod as 'automatic' | 'manual' | undefined, description: sessionParams.description, successUrl: sessionParams.successUrl ?? buildPaymentLinkReturnUrl(paymentLinkUrl, 'success'), cancelUrl: sessionParams.cancelUrl ?? buildPaymentLinkReturnUrl(paymentLinkUrl, 'cancelled'), metadata: sessionParams.metadata, providerInput: sessionParams.providerInput, organizationId: link.organizationId, tenantId: link.tenantId, }) const linkTransaction = em.create(GatewayPaymentLinkTransaction, { paymentLinkId: link.id, transactionId: transaction.id, customerEmail: parsed.data.email.trim().toLowerCase(), customerData: { firstName: parsed.data.firstName?.trim() || null, lastName: parsed.data.lastName?.trim() || null, phone: parsed.data.phone?.trim() || null, companyName: parsed.data.companyName?.trim() || null, acceptedTerms: parsed.data.acceptedTerms ?? false, customFormFields: parsed.data.customFormFields ?? null, }, }) em.persist(linkTransaction) link.useCount = (link.useCount ?? 0) + 1 await em.flush() await emitPaymentGatewayEvent('payment_gateways.payment_link.session_created', { paymentLinkId: link.id, paymentLinkToken: link.token, paymentLinkUrl, transactionId: transaction.id, paymentId: transaction.paymentId, providerKey: transaction.providerKey, organizationId: link.organizationId, tenantId: link.tenantId, }) return NextResponse.json({ transactionId: transaction.id, redirectUrl: session.redirectUrl ?? null, clientSecret: session.clientSecret ?? null, }, { status: 201 }) } catch (error) { console.error('[payment_link_pages] POST /pay/[token]/session error:', error) return NextResponse.json({ error: 'Internal server error' }, { status: 500 }) } } export const openApi = { tags: ['Payment Link Pages'], summary: 'Create a payment session from a multi-use payment link', methods: { POST: { summary: 'Create session for multi-use payment link', tags: ['Payment Link Pages'], responses: [ { status: 201, description: 'Session created' }, { status: 404, description: 'Payment link not found' }, { status: 409, description: 'Link not eligible for session creation' }, { status: 422, description: 'Invalid payload' }, ], }, }, } export default POST